# -*- coding:utf-8 -*-
# 多进程运行用例, 然后返回运行完的用例列表.

import re
import time
import json
import urllib2
from urllib2 import HTTPError
from urllib2 import URLError
from threading import Thread
from project_tools.resultHandler import ResultHandler

def run_all(domain_list):
    print "- " * 30
    print "Total(CC_Num) Pass Fail(timeout) Rate (TPS/Resp_time/Elapsed) TestSet"
    print "- " * 30
    # 优化: 先并发运行非dialog领域, 再并发运行is_dialog领域(一个领域一个线程).
    dialog_map = {} # {domain_0: case_num, domain_1: case_num}
    thread_num = 1
    for domain in domain_list:
        if domain.is_dialog:
            dialog_map[domain] = domain.total
        else:
            run_domain_in_cc(domain)
            thread_num = domain.thread_num
    run_dialog_in_cc(dialog_map, thread_num)

def run_dialog_in_cc(dialog_map, thread_num):
    ''' 按case数降序排序, 多个dialog多个线程 && 一个domain一个线程 '''
    domain_num = len(dialog_map)
    dialog_domains = sorted_domains(dialog_map)
    
    out_loop = domain_num / thread_num + 1
    for j in range(out_loop):
        threads = []
        for i in range(0, thread_num):
            index = j*thread_num + i
            if index > domain_num - 1:
                break
            domain = dialog_domains[index]
            t = Thread(target=run_dialog_domain, args=(domain,))
            threads.append(t)
        for x in threads:
            x.start()
        for x in threads:
            x.join()

def run_dialog_domain(domain):
    begin = time.time()
    for case in domain.case_list:
        run(case)
    domain.elapsed_time = time.time() - begin
    print_static_result(domain)

def sorted_domains(dialog_map):
    ''' 按用例数从多到少排序, 返回列表'''
    domains = []
    # [(d1, 300), (d2, 200), (d3, 100)]
    for d, num in sorted(dialog_map.iteritems(), key=lambda d:d[1], reverse=True):
        domains.append(d)
    return domains

def run_domain_in_cc(domain):
    """Run multiple threads that grouped by udid"""
    begin = time.time()
    thread_num = domain.thread_num

    all_case = domain.case_list
    case_num = len(all_case)
    # 按照并发数平均切分用例列表
    step = case_num / thread_num + 1
    m = 0
    n = step
    threads = []
    for i in range(0, thread_num + 1):
        if n > case_num:
            n = case_num
        case_list = all_case[m:n]
        if not case_list:
            break
        t = Thread(target=run_case_list, args=(case_list,))
        threads.append(t)
        m += step
        n += step
    for x in threads:
        x.start()
    for x in threads:
        x.join()

    domain.elapsed_time = time.time() - begin
    print_static_result(domain)

def retry_timeout_case(domain_list):
    for domain in domain_list:
        for case in domain.case_list:
            if case.is_timeout:
                run(case)

def update_static(domain_list):
    ''' retry_timeout_case 后需要重新统计 '''
    for d in domain_list:
        d.pass_ = 0
        d.fail = 0
        d.recall = 0
        d.timeout = 0
        for case in d.case_list:
            d.total_time += case.resp_time
            if case.is_pass:
                d.pass_ += 1
            else:
                d.fail += 1
            if case.recall_pass:
                d.recall += 1
            if case.is_timeout:
                d.timeout += 1
        
        d.resp_time = d.total_time * 1000 / d.total
        d.pass_rate = float(d.pass_) * 100 / d.total
        d.tps = int(d.total / d.elapsed_time)

def print_static_result(domain):
    """计算单个领域的用例准确率/召回率/响应时间等"""
    d = domain
    for case in d.case_list:
        d.total_time += case.resp_time
        if case.is_pass:
            d.pass_ += 1
        else:
            d.fail += 1
        if case.recall_pass:
            d.recall += 1
        if case.is_timeout:
            d.timeout += 1
    
    d.resp_time = d.total_time * 1000 / d.total
    d.pass_rate = float(d.pass_) * 100 / d.total
    d.tps = int(d.total / d.elapsed_time)

    c1 = str('%d(%dcc)'% (d.total, d.thread_num))
    c2 = str(d.pass_)
    c3 = str('%d(%d)' % (d.fail, d.timeout)) 
    c4 = str('%.2f' % d.pass_rate)
    c5 = str('%.2fhits/s_%.2fms_%.2fs' % (d.tps, d.resp_time, d.elapsed_time))
    c6 = d.name
    # "Total(CC) Pass Fail(out) Rate TestSet(QPS/Resp/Elapsed)"
    print "%-12s%-6s%-8s%-10s%-31s%-s" % (c1, c2, c3, c4, c5, c6)

#################################################################
#################### Run Case And Record Case Results ###########
#################################################################

JoinRe = '":\[?"?[^"]*'
NotNull = 'notnull'
Null = 'none'

def run_case_list(case_list):
    for case in case_list:
        run(case)

def run(case):
    ''' -> URL可以打开:
            结果匹配   : 记录响应时间 + 记录Case为Pass.
            结果不匹配 : 记录响应时间 + 记录Case为Fail. 不匹配列表赋值给case.unmatch_list.
            召回率计算  用service=xxx做为检查点
        -> URL无法打开 :
            重试3次, 全超时记为Case.isTimeout=True
    '''
    _request(case)

    if case.resp_result:
        case.is_timeout = False # for rerun case
        if case.kv_checks:
            _drop_checks_data(case) # json格式检查点, 把data置空
            check_fail_list = _get_unmatch_result(case.kv_checks, case.resp_result)
            recall_fail_list = _get_unmatch_result(case.recall_point, case.resp_result)
            case.unmatch_list = check_fail_list
            case.recall_detail = recall_fail_list
            if not check_fail_list:
                case.is_pass = True
            if not recall_fail_list:
                case.recall_pass = True
        else: # 无检查点, 直接判断为Pass
            case.is_pass = True
            case.recall_pass = True
    else:
        case.is_timeout = True

def _request(case):
    """ open url (retry mechanism) """
    retry = 1
    req = None
    ret = ""
    resp_time = None
    case.req_code = "" # before retry: clean status
    for i in range(retry):
        try:
            begin = time.time()
            req = urllib2.urlopen(case.encode_url, timeout=5)
            resp_time = time.time() - begin
            case.req_code += 'url send ok '
            ret = req.read()
            # case.req_code += str(req.code)
            if 'html' in ret: # 过滤掉url中的html文件.
                ret = re.sub(r"<!DOCTYPE html><html.*html>", "", ret)
            if ret.strip():
                break
        except HTTPError, e:
            if i == retry - 1:
                case.req_code += str(e.code)
                print "[%s] -> %s -> %s" % (e, case.txt, case.encode_url)
        except URLError, e:
            if i == retry - 1:
                case.req_code += str(e.reason)
                print "[%s] -> %s -> %s" % (e, case.txt, case.encode_url)
        except Exception, e:
            # print e, type(e)
            time.sleep(0.1 * (i+1)) # sleep longer if retry failed
            if i == retry - 1:
                case.req_code += str(type(e))
                print "[%s] -> %s -> %s" % (e, case.txt, case.encode_url)
        finally:
            if req:
                req.close()

    if resp_time is None:
        resp_time = time.time() - begin
    
    case.resp_time = resp_time
    case.resp_result = ret

def _has_unequal_express(check_str):
    if "!=" in check_str or "=!" in check_str:
        return True
    return False

def _replace_all_unequal(check_str):
    return check_str.replace("=!", "!=")

def _is_key_value_check_points(check_str):
    if _is_json_check_points(check_str):
        return False
    if _is_regular_check_points(check_str):
        return False
    return True

def _is_regular_check_points(check_str):
    if check_str.startswith('<RE') and check_str.endswith('/RE>'):
        return True
    return False

def _is_json_check_points(check_str):
    if check_str.startswith('{') and check_str.endswith('}'):
        return True
    return False

def _get_unmatch_result(checks, actual_str):
    '''
    支持的检查点格式: 
    1. key=value1|value2(竖线表示"或");
    2. key=!value1|value2或key!=value1|value2(竖线表示"与")
    3. json字符串, 和json数组;
    4. 已经写好的正则表达式, 需要用<RE key=regular /RE>标签;

    返回不匹配的key,value列表; 列表为空时, 表示所有检查点Pass.
    '''
    checks = checks.strip()

    # 99%是key=value格式标注格式
    if _is_key_value_check_points(checks):
        return _kv_unmatch_result(checks, actual_str)
    # json/json数组格式检查点: diff_log
    if _is_json_check_points(checks): # json
        return _json_unmatch_result(checks, actual_str)
    # 检查点是已经写好的正则表达式 <RE ... /RE>
    if _is_regular_check_points(checks):
        return _re_unmatch_result(checks, actual_str)

def _kv_unmatch_result(check_str, actual_str):
    """
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    来个笑话    quitDialog=None;service=chat|poem;
    对应的check_map:
    {
        "quitDialog=None": {'quitDialog' : ['+']},
        "service=chat|poem": {'service', ['chat', 'poem']},
    }
    新增支持多组检查点(双竖线分割): service=audio;tag=灰太狼||service=video;tag=喜羊羊
    返回check_map列表
    [
        { "service=audio":{"service":['audio']},
          "tag=灰太狼":{"tag":["灰太狼"]}
        },
        { "service=video":{"service":['video']},
          "tag=喜羊羊":{"tag":["喜羊羊"]}
        },
    ] 
    """
    
    if _has_unequal_express(check_str):
        check_str = _replace_all_unequal(check_str)
    check_map_list = __kv_check_map_list(check_str)
    # print check_map_list
    # print '-' * 20
    buffer_list = []
    for check_map in check_map_list:
        unmatch_buffer = []
        for origin_kv, kv_map in check_map.items():
            for key, v_list in kv_map.items():
                if not key.endswith('!'): # positive
                    regular_list = [JoinRe.join([key, v]) for v in v_list]
                    or_count = 0
                    for regular in regular_list:
                        if re.search(regular, actual_str, re.IGNORECASE):
                            break # 支持'或'方式(key=value1|value2), 有一个匹配即可!
                        else:
                            or_count += 1
                    # 如果所有"或"都没有匹配, 那么此检查点失败.
                    if or_count == len(regular_list):
                        unmatch_buffer.append(origin_kv)
                else: # negative
                    regular_list = [JoinRe.join([key.strip("!"), value]) for value in v_list]
                    if not v_list: # v_list为空, 表示只判断key不存在
                        regular_list = [JoinRe.join([key.strip("!")])]
                    
                    for regular in regular_list:
                        if re.search(regular, actual_str, re.IGNORECASE):
                            unmatch_buffer.append(origin_kv)
                        else:
                            continue # 支持'|'方式(key!=v1|v2), 表示全不匹配(与).
        if unmatch_buffer:
            buffer_list.append(unmatch_buffer)
        else:
            # 多组检查的匹配一组即可, 默认双竖线为"或", 需要清空unmatch_list
            return []
    # 如果召回, 但未通过, 那么只显示未通过的一组检查点.
    for buff in buffer_list:
        if 'service=' not in ';'.join(buff):
            return buff
    # 如果全部召回失败, 显示最所有未通过检查点
    unmatch_list = []
    for buff in buffer_list:
        unmatch_list += buff
    return unmatch_list

def _json_unmatch_result(checks, actual_str):
    kv_pairs = _extract_kv_pairs(checks)
    return _kv_unmatch_result(kv_pairs, actual_str)

def _re_unmatch_result(checks, actual_str):
    unmatch_list = []
    for regular in checks[3:-4].strip().split(';'):
        if not re.search(regular, actual_str, re.IGNORECASE):
            unmatch_list.append(regular)
    return unmatch_list

def _get_jkey_list(key_file='conf/keys.conf'):
    jkey_list = []
    for ln in open(key_file):
        ln = ln.strip()
        if not ln:
            continue
        if ln.startswith('#'):
            continue
        if ln not in jkey_list:
            jkey_list.append(ln)
    return jkey_list

def _extract_kv_pairs(checks):
    ''' 从json_str里提前key_value, key是已经整理好的full_regression里的key '''
    kv_pairs = {}
    jkey_list = _get_jkey_list()
    
    for kv_colon_pair in re.findall(r'"[^"]+":"?[^",\[\{:]+"?', checks):
        key, value = kv_colon_pair.split(':')
        key = key.strip('"')
        value = value.strip('"')
        if key == 'text':
            value = 'NotNull'
        if key == 'service':
            value = value.replace('cn.yunzhisheng.', '')
        kv_pair = '%s=%s' % (key, value)
        if key in jkey_list and not kv_pairs.has_key(key):
            kv_pairs[key] = value
    kv_checks = ''
    for k,v in kv_pairs.items():
        kv_checks += '%s=%s;' % (k,v)
    return kv_checks

def _drop_checks_data(case):
    ''' 如果用例是json格式检查点, 那么data, responseId, history都置为空 '''
    if not case.kv_checks.startswith('{'):
        return
    try:
        js_load = json.loads(case.kv_checks)
        js_load['data'] = ''
        js_load['sessionId'] = ''
        js_load['responseId'] = ''
        js_load['history'] = ''
        case.kv_checks = str(js_load)
    except Exception as e:
        print e
        print case.kv_checks
        print '\n\n'
        return

def __kv_check_map_list(key_value_str):
    '''
    1. 用'='分割每个检查点, 路径格式的'/'替换为'.{3,}', 右边用'='合并为一个字符串(处理http://xxxx=yyyyy)
    2. 替换左右两边的特殊字符.
    3. 重新用'.*:.*'合并检查点.
    4. 忽略没有value的检查点(如"content=;")和key = normalHeader的检查点.
    5. 如果检查点key=value对中的value中有空格，去掉value中的空格
    6. 通过对比java版本发现以下字段也被忽略了.... 见continue中关键字.
    7. test/musicbox/test_musicbox_scene.txt中的85行中"code-SEARCH_SONG"应该为"code=SEARCH_SONG".
    加入判断, 不含"="号, 自动忽略检查点
    8. 2016.4, 新增检查点支持'或'标记, 带"|"的一个检查点先被转换为一个字典:{'key':['value1', 'value2', ...]}
    9. 2017.3, text检查点中包含小括号'(' / ')'时, 替换为"\\(", 避免触发python re的unbalanced parenthesis错误.
    10. 新增!=不等于. 本函数无变化.
    11. 支持非空字段: property=NOTNULL
    12. bugfix: 如果value是各位数字(如"focusDateIndex":1), 那么单独判断, 更新正则为: focusDateIndex[^:]+:1
    13. 2019.1.28 : key=<value_regular>, value为正则表达式, 语法为尖括号.
    14. 2019.9.22 : key=null, 表示这个key不(应该)存在.
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    来个笑话    quitDialog=Null;service=chat|poem;
    对应的返回的字典结构为:
    {
        "quitDialog=Null": {'quitDialog' : ['+']},
        "service=chat|poem": {'service', ['chat', 'poem']},
    }

    用双竖线分割多组检查点:
    新增支持多组检查点(双竖线分割): service=audio;tag=灰太狼||service=video;tag=喜羊羊
    返回map列表为:   [{"service=audio":{"service":['audio']},
                        "tag=灰太狼":{"tag":["灰太狼"]}
                        },
                        {"service=video":{"service":['video']},
                           "tag=喜羊羊":{"tag":["喜羊羊"]}, 
                    ]   

    '''
    # 先映射原始检查点和语法解析后的检查点).再进一步处理
    check_map_list = []
    for kv_str in key_value_str.split("||"):
        check_map = {}
        kv_list = kv_str.strip().split(';')
        for kv in kv_list:
            # 先过滤掉一些无效检查点
            if kv == "": continue
            if kv.endswith('='): continue
            if "normalHeader" in kv: continue
            if "keyword" in kv: continue # keyword拆分不是100%正确.
            # localSearch领域检查点用逗号分割的sort/city, 现在的sort使用sortBiz关键字, 故忽略.
            if "sort" in kv and 'city' in kv: continue
            # weather领域的city=CURRENT_CITY忽略.
            if "city=CURRENT_CITY" in kv: continue
            # 初步清洗检查点
            if '==' in kv:
                kv = kv.replace('==', '=') # 个别检查点标注错误, 使用了双等号
            if kv.startswith('text') and ('(' in kv or ')' in kv):
                kv = kv.replace('(', '\(')
                kv = kv.replace(')', '\)')
            if '=' in kv: # 必须key=value格式才作为有效检查点.
                check_map[kv] = __clean_checks(kv)
        check_map_list.append(check_map)
    return check_map_list

def __clean_checks(kv):
    ''' 进一步清洗和转换kv为字典格式:
        service=chat|poem 转为 {'service', ['chat', 'poem']}
    '''
    splits = kv.strip().split('=')
    k = splits[0]
    # 替换特殊字符
    k = re.sub(r'[*?+()^{}$\\]', '.', k)
    # 空格单独处理,替换为?, 匹配路径/: 如general/intent/type=T
    if " " in k:
        k = k.replace(' ', '\s?')
    if '/' in k:
        k = k.replace('/', '.{3,}')
    # values: 去除'='后的空串, 部分url含'=', 导致'='分割后导致url不完整, 需要重新用'='join连接
    v_list = [v for v in splits[1:] if v.strip()]
    v_list = '='.join(v_list).split('|')
    # 进行封装
    for i in range(len(v_list)):
        v = v_list[i]
        # value是写好的正则表达式, 直接去掉<>标签
        if v.startswith('<') and v.endswith('>'):
            v_list[i] = v.strip().strip('<>')
        else:
            # 替换特殊符号和空格
            v_list[i] = re.sub(r'[*?+()^${}\\]', '.', v)
            if " " in v:
                v_list[i] = v_list[i].replace(' ', '\s?')
            # 支持非空字段检查
            if NotNull == v.lower():
                v_list[i] = '[^"]+'
            # 支持空(即该字段不应该存在), key为!(negative)
            # 同时v_list只有一个值: '+'
            if Null == v.lower():
                k += '!'
                v_list = []

    return {k: v_list}

def __chs_is_in_str(str_maybe_with_chs):
    for ch in str_maybe_with_chs.decode('utf-8'):
        if u'\u4e00' <= ch <= u'\u9fff': return True
    return False

def __get_chinese_char_regular(str_with_CHS):
    """
    搜索关键字被拆分为多个key, 如:　＂韩都衣舍女装毛呢连衣裙＂　返回　＂{"keywords":{"key1":"韩都衣舍","key2":"女装","key3":"毛呢","key4":"连衣裙"}＂
    这时候去掉key1/key2/key3/key4, 直接提取所有汉字用.*连接: 即"keywords"韩.*都.*衣.*舍.*女.*装.*毛.*呢.*连.*衣.*裙"
    """
    chsRegExp = ""
    for chs_char in re.findall(ur'[\u4e00-\u9fff]+', str_with_CHS):
        chsRegExp += chs_char
    chsRegExp = ".*".join(list(chsRegExp))
    return chsRegExp
